﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Describes one part of the composition.
    /// </summary>
    public class PartDescriptorElement : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="PartDescriptorElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="CompositionElement"/>
        /// </para>
        /// <para>
        /// The composition that directly owns this part descriptor.
        /// </para>
        /// </param>
        /// <param name="name">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The name of the part that is being described by this part descriptor.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// <paramref name="name"/> is not a valid name for the descriptor.
        /// Please, refer to the <see cref="Name"/> property documentation for details.
        /// </exception>
        internal PartDescriptorElement(CompositionPart packagePart, CompositionElement owner, string name)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            Name = name;

            keyChangements = new ElementCollection<KeyChangementIndicationElement>(this, keyChangementsElement);
            customDataCollection = new ElementCollection<CustomPartDataElement>(this, customDataElement);

            KeyIndication = new KeyIndicationElement(packagePart, this, new double[] { 0, 0, 0, 0, 0, 0, 0 });
            keyChangements.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(keyChangements_CollectionChanged);
        }

        /// <summary>
        /// Creates a new <see cref="PartDescriptorElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="CompositionElement"/>
        /// </para>
        /// <para>
        /// The composition that directly owns this part descriptor.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal PartDescriptorElement(CompositionPart packagePart, CompositionElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (!(owner is CompositionElement))
                throw new ArgumentException("Owner has to be a CompositionElement!", "owner");

            abbrevAttribute = element.Attributes[PackageVocabulary.AbbreviationAttribute];
            nameAttribute = element.Attributes[PackageVocabulary.NameAttribute];

            keyIndicationElement = element["key", PackageVocabulary.PackageNamespaceUri];
            keyIndication = new KeyIndicationElement(PackagePart, this, keyIndicationElement.FirstChild as XmlElement);

            keyChangementsElement = element["key_changements", PackageVocabulary.PackageNamespaceUri];
            keyChangements = new ElementCollection<KeyChangementIndicationElement>(this, keyChangementsElement);

            customDataElement = element[PackageVocabulary.CustomPartDatElement, PackageVocabulary.PackageNamespaceUri];
            customDataCollection = new ElementCollection<CustomPartDataElement>(this, customDataElement);

            keyChangements.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(keyChangements_CollectionChanged);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.PartDescriptorElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            abbrevAttribute = document.CreateAttribute(PackageVocabulary.AbbreviationAttribute);
            nameAttribute = document.CreateAttribute(PackageVocabulary.NameAttribute);

            customDataElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.CustomPartDatElement,
                PackageVocabulary.PackageNamespaceUri
                );

            keyIndicationElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                "key",
                PackageVocabulary.PackageNamespaceUri
                );

            keyChangementsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                "key_changements",
                PackageVocabulary.PackageNamespaceUri
                );

            element.Attributes.Append(abbrevAttribute);
            element.Attributes.Append(nameAttribute);

            element.AppendChild(keyIndicationElement);
            element.AppendChild(keyChangementsElement);
            element.AppendChild(customDataElement);            

            return element;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Handles the CollectionChanged event of the keyChangements control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="Notoric.Model.Collections.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        private void keyChangements_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Replace:
                    foreach (KeyChangementIndicationElement key in e.NewItems)
                        RaiseKeyChanged(key.KeyIndication, key.Measure);
                    break;
                case NotifyCollectionChangedAction.Remove:
                {
                    var keyIndex = e.OldStartingIndex;
                    if (keyIndex > 0)
                        RaiseKeyChanged(keyChangements[keyIndex - 1].KeyIndication, keyChangements[keyIndex - 1].Measure);
                    else
                        RaiseKeyChanged(keyIndication, null);
                    break;
                }
                case NotifyCollectionChangedAction.Reset:
                    RaiseKeyChanged(keyIndication, null);
                    break;
            }
        }

        #endregion

        #region Public Events

        /// <summary>
        /// Occurs when the key of the part has changed.
        /// </summary>
        /// <remarks>
        /// This event occurs when either of the <see cref="KeyIndication"/> property
        /// or the <see cref="KeyChangements"/> collections has changed.
        /// </remarks>
        public event EventHandler<KeyChangedEventArgs> KeyChanged;

        #endregion

        #region Private Methods

        /// <summary>
        /// Raises the key changed.
        /// </summary>
        private void RaiseKeyChanged(KeyIndicationElement key, MeasureElement from)
        {
            var composition = FindOwner<CompositionElement>();

            if (composition.MeasuresCount == 0)
                return;
            if (from == null)
                from = composition[1];
            
            int index = 0;
            for (; index < keyChangements.Count && keyChangements[index].Measure.Number <= from.Number; ++index) ;
            MeasureElement end = (index < keyChangements.Count ? keyChangements[index].Measure : composition[composition.MeasuresCount]);

            ((CompositionPart)PackagePart).RaiseGlobalChangePending(from, end);

            if (KeyChanged != null)
                KeyChanged(this, new KeyChangedEventArgs(key, from, end));
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Searches the <see cref="CustomData"/> collection for a first occurence of an element with type <typeparamref name="T"/>.
        /// </summary>
        /// <typeparam name="T">The type of the custom data part being searched (must be a subtype of <see cref="CustomPartDataElement"/>).</typeparam>
        /// <returns>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// A reference to the first custom data part that satisfies the condition or <see langword="null"/> if none is found.
        /// </para>
        /// </returns>
        public T FindCustomData<T>() where T : CustomPartDataElement
        {
            T data = (T)customDataCollection.FirstOrDefault(cd => cd is T);

            return data;
        }

        /// <summary>
        /// Inserts a key changement to the correct position in the <see cref="KeyChangements"/> collection.
        /// </summary>
        /// <param name="keyChangement">
        /// <para>
        /// Type: <see cref="KeyChangementIndicationElement"/>
        /// </para>
        /// <para>
        /// The key changement.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="keyChangement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="keyChangement"/> does not belong to this part descriptor.
        /// </exception>
        public void InsertKeyChangement(KeyChangementIndicationElement keyChangement)
        {
            if (keyChangement == null)
                throw new ArgumentNullException("keyChangement");
            if (keyChangement.FindOwner<PartDescriptorElement>() != this)
                throw new ArgumentException("The element does not belong to this part descriptor.", "keyChangement");

            int index = 0;
            for (; index < keyChangements.Count && keyChangements[index].Measure.Number < keyChangement.Measure.Number; ++index) ;
            keyChangements.Insert(index, keyChangement);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the abbreviation of the part's name.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The abbreviation of the part's name. The abbreviation can be used in the visualization
        /// instead of the name of the part.
        /// </para>
        /// </value>
        public string Abbreviation
        {
            get { return abbrevAttribute.Value; }
            set
            {
                abbrevAttribute.Value = value;
                RaisePropertyChanged("Abbreviation");
            }
        }

        /// <summary>
        /// Gets the collection of custom data elements in this part descriptor.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="CustomPartDataElement"/>
        /// </para>
        /// <para>
        /// The collection of custom part data elements. The custom data elements can be used
        /// to extend the definition of a part descriptor by custom data fields. For example
        /// the MIDI instrument that plays this part, the number of staves, etc.
        /// </para>
        /// </value>
        /// <seealso cref="CustomPartDataElement"/>
        public ElementCollection<CustomPartDataElement> CustomData
        {
            get { return customDataCollection; }
        }

        /// <summary>
        /// Gets the key changements.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="KeyChangementIndicationElement"/>
        /// </para>
        /// <para>
        /// An ordered (from first to last) collection of changements of this part's key.
        /// </para>
        /// </value>
        public ElementCollection<KeyChangementIndicationElement> KeyChangements
        {
            get { return keyChangements; }
        }

        /// <summary>
        /// Gets or sets the indication of the key of this part.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="KeyIndicationElement"/>
        /// </para>
        /// <para>
        /// The indication of the key of this part.
        /// </para>
        /// </value>
        public KeyIndicationElement KeyIndication
        {
            get { return keyIndication; }
            set
            {
                if (keyIndication != null)
                    keyIndicationElement.RemoveChild(keyIndication.Element);

                keyIndication = value;

                if (keyIndication != null)
                    keyIndicationElement.AppendChild(keyIndication.Element);

                RaisePropertyChanged("KeyIndication");
                RaiseKeyChanged(keyIndication, null);
            }
        }
        
        /// <summary>
        /// Gets or sets the name of the part described by this descriptor.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The name of the part that is being described by this descriptor.
        /// This value cannot be <see langword="null"/> or empty ("").
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is <see langword="null"/> or an empty string ("").
        /// </exception>
        /// TODO: Write the validation rules.
        public string Name
        {
            get { return nameAttribute.Value; }
            set
            {
                ValidateAndThrow("Name", value);
                nameAttribute.Value = value;

                RaisePropertyChanged("Name");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Abbreviation"/> property.
        /// </summary>
        private XmlAttribute abbrevAttribute;
        /// <summary>
        /// Holds the value of the <see cref="CustomData"/> property.
        /// </summary>
        private ElementCollection<CustomPartDataElement> customDataCollection;
        /// <summary>
        /// Holds the XML element that holds the <see cref="customDataCollection"/>.
        /// </summary>
        private XmlElement customDataElement;
        /// <summary>
        /// Holds the value of the <see cref="KeyChangements"/> property.
        /// </summary>
        private ElementCollection<KeyChangementIndicationElement> keyChangements;
        /// <summary>
        /// Holds the XML element that holds the <see cref="KeyChangements"/> collection.
        /// </summary>
        private XmlElement keyChangementsElement;
        /// <summary>
        /// Holds the value of the <see cref="KeyIndication"/> property.
        /// </summary>
        private KeyIndicationElement keyIndication;
        /// <summary>
        /// Holds the XML element that holds the value of the <see cref="KeyIndication"/> property.
        /// </summary>
        private XmlElement keyIndicationElement;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Name"/> property.
        /// </summary>
        private XmlAttribute nameAttribute;

        #endregion
    }

    /// <summary>
    /// Holds the data of the <see cref="PartDescriptorElement.KeyChanged"/> event
    /// </summary>
    public class KeyChangedEventArgs : EventArgs
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="KeyChangedEventArgs"/> class.
        /// </summary>
        /// <param name="key">
        /// <para>
        /// Type: <see cref="KeyIndicationElement"/>
        /// </para>
        /// <para>
        /// The key.
        /// </para>
        /// </param>
        /// <param name="from">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The first measure where <paramref name="key"/> is valid.
        /// </para>
        /// </param>
        /// <param name="to">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The last measure where <paramref name="key"/> is valid.
        /// </para>
        /// </param>
        public KeyChangedEventArgs(KeyIndicationElement key, MeasureElement from, MeasureElement to)
        {
            Key = key;
            FromMeasure = from;
            ToMeasure = to;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the first measure where the <see cref="Key"/> is valid.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The first measure where the new <see cref="Key"/> is valid.
        /// </para>
        /// </value>
        public MeasureElement FromMeasure
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the new key indication.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="KeyIndicationElement"/>
        /// </para>
        /// <para>
        /// The new key indication.
        /// </para>
        /// </value>
        public KeyIndicationElement Key
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the last measure where the <see cref="Key"/> is valid.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// The last measure where the new <see cref="Key"/> is valid.
        /// </para>
        /// </value>
        public MeasureElement ToMeasure
        {
            get;
            private set;
        }

        #endregion
    }
}
